/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.jfonia.view;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.jfonia.commands.AddLabelCommand;
import org.jfonia.commands.RemoveLabelCommand;
import org.jfonia.connect5.basics.BasicValueNode;
import org.jfonia.connect5.basics.MutableValueNode;
import org.jfonia.connect5.basics.Observer;
import org.jfonia.connect5.basics.ValueNode;
import org.jfonia.connect5.intervals.BasicDoubleInterval;
import org.jfonia.connect5.numerical.DoubleMaxRelation;
import org.jfonia.connect5.numerical.DoubleMultiplicationRelation;
import org.jfonia.connect5.numerical.DoubleSumRelation;
import org.jfonia.connect5.numerical.UnidirectionalDoubleSumRelation;
import org.jfonia.connect5.relations.Equal;
import org.jfonia.connect5.relations.UnidirectionalEqual;
import org.jfonia.constants.ViewConstants;
import org.jfonia.view.elements.IElement;
import org.jfonia.view.labels.Label;
import org.jfonia.view.panels.BasicNodePanel;
import org.jfonia.view.panels.ViewPanel;
import org.jfonia.view.relations.DoubleInversionRelation;
import percussiongenerator.model.Pair;

/**
 *
 * @author Rik Bauwens
 */
public class RelationBuilder implements ITicksToXMapperObserver
{
        private ViewPanel viewPanel;
        private StaffCollectionView staffCollectionView;

        private MutableValueNode<Double> xNode;
        private BasicDoubleInterval preStaffInterval;
        private Equal<Double> xRelation;
        private MutableValueNode<Double> preferredWidthNode;
        private Equal<Double> preferredWidthRelation;

        private BasicDoubleInterval headerInterval;

        private Map<SequenceView, TicksToXMapper> ticksToXMappers;
        private Map<Integer, List<Pair<DoubleMaxRelation, IElement>>> maxRelations;

        //List of coordinates for the corresponding frames
        private TreeMap<Integer, Pair<BasicDoubleInterval, Set<IElement>>> frameIntervals;
        //List of frame header panels
        private Map<Integer, Pair<FrameBoxPanel, FrameBoxPanel>> boxPanels;
        //Auxiliary data structure to store the end tick for each musicalelement
        private Map<IElement, Pair<Integer, Integer>> elementTicks;

        public RelationBuilder(MutableValueNode<Double> xNode, MutableValueNode<Double> widthNode,
                               ViewPanel viewPanel, BasicDoubleInterval headerInterval,
                               StaffCollectionView staffCollectionView)
        {
                this.viewPanel = viewPanel;
                this.staffCollectionView = staffCollectionView;

                this.xNode = new BasicValueNode<Double>(0.0);
                preStaffInterval = new BasicDoubleInterval();
                new Equal<Double>(xNode, preStaffInterval.getBeginNode());
                new Equal<Double>(preStaffInterval.getEndNode(), this.xNode);
                preStaffInterval.setDifference(ViewConstants.STAFF_BOX_LEFT_MARGIN
                                      + ViewConstants.STAFF_BOX_WIDTH
                                      + ViewConstants.STAFF_BOX_RIGHT_MARGIN);
                preferredWidthNode = new BasicValueNode<Double>(0.0);
                BasicDoubleInterval postStaffInterval = new BasicDoubleInterval();
                new Equal<Double>(preferredWidthNode, postStaffInterval.getBeginNode());
                postStaffInterval.setDifference(ViewConstants.STAFF_RIGHT_MARGIN);
                new Equal<Double>(postStaffInterval.getEndNode(), widthNode);
                //new Equal<Double>(widthNode, preferredWidthNode);

                this.headerInterval = headerInterval;
                headerInterval.setDifference(ViewConstants.FRAME_BOX_TOP_MARGIN
                                           + ViewConstants.FRAME_BOX_WIDTH
                                           + ViewConstants.FRAME_BOX_BOTTOM_MARGIN);

                ticksToXMappers = new HashMap<SequenceView, TicksToXMapper>();
                maxRelations = new HashMap<Integer, List<Pair<DoubleMaxRelation, IElement>>>();
                frameIntervals = new TreeMap<Integer, Pair<BasicDoubleInterval, Set<IElement>>>();
                boxPanels = new HashMap<Integer, Pair<FrameBoxPanel, FrameBoxPanel>>();

                elementTicks = new HashMap<IElement, Pair<Integer, Integer>>();
        }

        public boolean hasElementFocus()
        {
                for(IElement element : elementTicks.keySet())
                {
                        if(element.hasFocus())
                                return true;
                }
                return false;
        }

        private MutableValueNode getXNode()
        {
                return xNode;
        }

        private void addTicksToXMapper(SequenceView sequenceView)
        {
                TicksToXMapper ticksToXMapper = new TicksToXMapper(this, xNode, preferredWidthNode, sequenceView);
                initializeRelations(ticksToXMapper);
                ticksToXMapper.addObserver(this);
                ticksToXMappers.put(sequenceView, ticksToXMapper);
        }

        private void removeTicksToXMapper(SequenceView sequenceView)
        {
                TicksToXMapper ticksToXMapper = ticksToXMappers.get(sequenceView);
                removeRelations(ticksToXMapper);
                ticksToXMapper.removeObserver(this);
                ticksToXMappers.remove(sequenceView);
        }

        public BasicDoubleInterval getPreStaffInterval()
        {
                return preStaffInterval;
        }

        public TicksToXMapper getMapper(SequenceView sequenceView)
        {
                if(!ticksToXMappers.containsKey(sequenceView))
                        addTicksToXMapper(sequenceView);
                return ticksToXMappers.get(sequenceView);
        }

        public double getX(int tick)
        {
                Integer beginTick = frameIntervals.floorKey(tick);
                Integer endTick = frameIntervals.higherKey(beginTick);

                double beginX = frameIntervals.get(beginTick).first.getEnd();
                if(endTick == null)
                        return beginX;

                double endX = frameIntervals.get(endTick).first.getBegin();

                double offset = (tick - beginTick) / ((endTick - beginTick) * 1.0);
                return beginX + (endX - beginX) * offset;
        }

        public void setBoxPanelSelected(Label label, boolean selected)
        {
                Pair<FrameBoxPanel, FrameBoxPanel> frameBoxPanels = boxPanels.get(label.getTick());
                if(label.isPre())
                {
                        frameBoxPanels.first.setSelected(selected);
                        frameBoxPanels.first.setLabel(label);
                }
                else
                {
                        frameBoxPanels.second.setSelected(selected);
                        frameBoxPanels.second.setLabel(label);
                }
        }

        private void initializeRelations(TicksToXMapper ticksToXMapper)
        {
                //Iterate over all ticks in the mapper
                for(IElement element : ticksToXMapper)
                        addRelation(element, ticksToXMapper);
        }

        private void addRelation(IElement element, TicksToXMapper ticksToXMapper)
        {
                //Check wheter this relation already exists, and return if it does
                if(checkExists(element))
                        return;

                int beginTick = element.getBegin();
                int endTick = element.getEnd();

                //Store the ticks
                elementTicks.put(element, new Pair(beginTick, endTick));

                //Create a new maximum relation
                Pair<DoubleMaxRelation, IElement> maxRelation
                        = new Pair<DoubleMaxRelation, IElement>();
                //Check whether the elements begin and endtick are the same
                if(beginTick == endTick)
                {
                        MutableValueNode<Double> widthNode = new BasicValueNode<Double>(0.0);
                        new UnidirectionalEqual<Double>(ticksToXMapper.getPreferredWidthNode(element), widthNode);
                        maxRelation.first = new DoubleMaxRelation(getPreFrameWidth(endTick, element), widthNode);
                }
                else
                {
                        MutableValueNode<Double> endNode = new BasicValueNode<Double>(0.0);
                        new UnidirectionalDoubleSumRelation(getBeginFrameX(beginTick, element),
                                              ticksToXMapper.getPreferredWidthNode(element),
                                              endNode);
                        maxRelation.first = new DoubleMaxRelation(getEndFrameX(endTick, element), endNode);
                }
                maxRelation.second = element;
                //Add a new key and array for the tick if it does not exist already
                if(!maxRelations.containsKey(beginTick))
                        maxRelations.put(beginTick, new ArrayList<Pair<DoubleMaxRelation, IElement>>());
                //Add the new relation to the map
                maxRelations.get(beginTick).add(maxRelation);

                /*for(Integer tick : frameIntervals.keySet())
                {
                        System.out.println("TICK: " + tick);
                        System.out.println(frameIntervals.get(tick).first.getBegin() + " * "
                                         + frameIntervals.get(tick).first.getDifference() + " * "
                                         + frameIntervals.get(tick).first.getEnd());
                        System.out.println(frameIntervals.get(tick).second);

                        System.out.println();
                }*/
                /*endNode.addObserver(new Observer(){
                        public void changed(Observable source)
                        {
                                for(Integer i : frameXNodes.keySet())
                                        System.out.println(i + ": " + frameXNodes.get(i).getValue());
                                System.out.println();
                        }
                });*/
                /*System.out.println("ADD -> " + element.hashCode() + "\t" + element.getBegin() + " - " + element.getEnd());
                System.out.println("pref: " + ticksToXMapper.getPreferredWidthNode(beginTick).getValue());
                System.out.println("pref0: " + ticksToXMapper.getPreferredWidthNode(0).getValue());
                for(IStaffElement staffElement : ((StaffPart) element).getStaffElements())
                        System.out.print(staffElement + ", ");
                System.out.println();
                for(Integer tick : maxRelations.keySet())
                {
                        System.out.print(tick + ": ");
                        for(Pair<DoubleMaxRelation, TicksToXMapper> maxRel : maxRelations.get(tick))
                                System.out.println("\t" + maxRel.first + " - " + maxRel.second);
                }
                System.out.println("frameXNodes");
                for(Integer i : frameXNodes.keySet())
                        System.out.println(i + ": " + frameXNodes.get(i) + " * " + frameXNodes.get(i).first.getValue());
                System.out.println();
                System.out.println();*/
        }

        private void removeRelations(TicksToXMapper ticksToXMapper)
        //Seperate implementation (removeRelation() is not used),
        //to ensure that no errors are caused, due to the removal of elements
        //from the collection that is being iterated through
        {
                //Use of an iterator instead of a foreach loop to be able
                //to safely remove an element within the iteration
                Iterator<Integer> tickIterator
                        = maxRelations.keySet().iterator();
                while(tickIterator.hasNext())
                {
                        Integer tick = tickIterator.next();
                        //The list of equalrelations corresponding with this tick
                        List<Pair<DoubleMaxRelation, IElement>> maxRelationsPerTick
                                = maxRelations.get(tick);

                        Iterator<Pair<DoubleMaxRelation, IElement>> relationIterator
                                = maxRelationsPerTick.iterator();
                        while(relationIterator.hasNext())
                        {
                                Pair<DoubleMaxRelation, IElement> maxRelation
                                        = relationIterator.next();
                                if(maxRelation.second == ticksToXMapper)
                                {
                                        maxRelation.first.switchOff();
                                        //Safely remove while iterating over the same collection
                                        relationIterator.remove();
                                }
                        }
                        if(maxRelationsPerTick.isEmpty())
                        {
                                //Safely remove while iterating over the same collection
                                tickIterator.remove();
                                //Remove basenode from auxiliary map
                                for(IElement element : ticksToXMapper.getElements(tick))
                                        removeFrameXInterval(tick, element);
                        }
                }
        }

        private void removeRelation(IElement element)
        {
                int beginTick = elementTicks.get(element).first;
                int endTick = elementTicks.get(element).second;
                //The list of equalrelations corresponding with this tick
                List<Pair<DoubleMaxRelation, IElement>> maxRelationsPerTick
                        = maxRelations.get(beginTick);

                Iterator<Pair<DoubleMaxRelation, IElement>> relationIterator
                        = maxRelationsPerTick.iterator();
                while(relationIterator.hasNext())
                {
                        Pair<DoubleMaxRelation, IElement> maxRelation
                                = relationIterator.next();
                        if(maxRelation.second == element)
                        {
                                maxRelation.first.switchOff();
                                //Safely remove while iterating over the same collection
                                relationIterator.remove();
                        }
                }
                if(maxRelationsPerTick.isEmpty())
                        maxRelations.remove(beginTick);
                removeFrameXInterval(beginTick, element);
                removeFrameXInterval(endTick, element);
                elementTicks.remove(element);
        }

        private boolean checkExists(IElement element)
        {
                boolean exists = false;
                int beginTick = element.getBegin();
                if(maxRelations.containsKey(beginTick))
                {
                        for(Pair<DoubleMaxRelation, IElement> maxRelation : maxRelations.get(beginTick))
                        {
                                if(maxRelation.second == element)
                                        exists = true;
                        }
                }
                return exists;
        }

        public ValueNode<Double> getPreFrameX(int beginTick, IElement element)
        {
                return getFrameInterval(beginTick, element).getBeginNode();
        }

        public MutableValueNode<Double> getPreFrameWidth(int beginTick, IElement element)
        {
                return getFrameInterval(beginTick, element).getDifferenceNode();
        }

        public ValueNode<Double> getBeginFrameX(int beginTick, IElement element)
        {
                return getFrameInterval(beginTick, element).getEndNode();
        }

        public MutableValueNode<Double> getEndFrameX(int endTick, IElement element)
        {
                return getFrameInterval(endTick, element).getBeginNode();
        }

        private BasicDoubleInterval getFrameInterval(int tick, IElement element)
        {
                //Create a node if it does not exist
                if(frameIntervals.containsKey(tick))
                {
                        frameIntervals.get(tick).second.add(element);
                }
                else
                {
                        BasicDoubleInterval interval = new BasicDoubleInterval();
                        Set<IElement> elements = new HashSet<IElement>();
                        elements.add(element);
                        frameIntervals.put(tick, new Pair(interval, elements));

                        //The first node always needs to be equal to the x node
                        Integer lowerTick = frameIntervals.lowerKey(tick);
                        if(lowerTick == null)
                        {
                                if(xRelation != null)
                                        xRelation.switchOff();
                                xRelation = new Equal<Double>(xNode, interval.getBeginNode());
                        }
                        else
                        {
                                setMainBoxPanel(lowerTick, createMainBoxPanel(frameIntervals
                                                .get(lowerTick).first.getEndNode(),
                                                interval.getBeginNode(), lowerTick));
                        }

                        //The last node represents the width
                        Integer higherTick = frameIntervals.higherKey(tick);
                        if(higherTick == null)
                        {
                                if(preferredWidthRelation != null)
                                        preferredWidthRelation.switchOff();
                                preferredWidthRelation = new Equal<Double>(interval.getEndNode(),
                                                                           preferredWidthNode);
                        }
                        else
                        {
                                setMainBoxPanel(tick, createMainBoxPanel(interval.getEndNode(),
                                        frameIntervals.get(higherTick).first.getBeginNode(), tick));
                        }
                        setPreBoxPanel(tick);
                        return interval;
                }
                return frameIntervals.get(tick).first;
        }

        private void removeFrameXInterval(int tick, IElement element)
        {
                if(frameIntervals.containsKey(tick))
                        frameIntervals.get(tick).second.remove(element);
                else
                        return;
                if(frameIntervals.get(tick).second.isEmpty())
                {
                        //The first node always needs to be equal to the x node
                        Integer lowerTick = frameIntervals.lowerKey(tick);
                        if(lowerTick == null)
                        {
                                //This node is the lowest
                                if(xRelation != null)
                                        xRelation.switchOff();
                                //Find a new lowest tick
                                Integer higherTick = frameIntervals.higherKey(tick);
                                if(higherTick != null)
                                        xRelation = new Equal<Double>(xNode,
                                                frameIntervals.get(higherTick).first.getBeginNode());
                                else
                                        xRelation = null;
                        }
                        else
                                setMainBoxPanel(lowerTick, null);


                        //The last node represents the width
                        Integer higherTick = frameIntervals.higherKey(tick);
                        if(higherTick == null)
                        {
                                //This node is the highest
                                if(preferredWidthRelation != null)
                                        preferredWidthRelation.switchOff();
                                //Find a new highest tick
                                lowerTick = frameIntervals.lowerKey(tick);
                                if(lowerTick != null)
                                        preferredWidthRelation = new Equal<Double>(
                                                frameIntervals.get(lowerTick).first.getEndNode(), preferredWidthNode);
                                else
                                        preferredWidthRelation = null;
                        }
                        else
                        {
                                if(lowerTick != null)
                                        setMainBoxPanel(lowerTick, createMainBoxPanel(
                                                frameIntervals.get(lowerTick).first.getEndNode(),
                                                frameIntervals.get(higherTick).first.getBeginNode(), lowerTick));
                        }
                        removeAllBoxPanels(tick);
                        frameIntervals.remove(tick);
                }
        }

        private FrameBoxPanel createMainBoxPanel(MutableValueNode<Double> xLeftNode,
                                                 MutableValueNode<Double> xRightNode, int tick)
        {
                MutableValueNode<Double> invertedNode = new BasicValueNode<Double>(0.0);
                MutableValueNode<Double> widthNode = new BasicValueNode<Double>(0.0);
                new DoubleInversionRelation(xLeftNode, invertedNode);
                new UnidirectionalDoubleSumRelation(xRightNode, invertedNode, widthNode);
                return new FrameBoxPanel(xLeftNode, headerInterval.getBeginNode(),
                                         widthNode, headerInterval.getDifferenceNode(),
                                         tick, false);
        }

        private void setPreBoxPanel(int tick)
        {
                FrameBoxPanel boxPanel = new FrameBoxPanel(frameIntervals.get(tick).first.getBeginNode(),
                                                           headerInterval.getBeginNode(),
                                                           frameIntervals.get(tick).first.getDifferenceNode(),
                                                           headerInterval.getDifferenceNode(),
                                                           tick, true);
                if(boxPanels.containsKey(tick))
                {
                        if(boxPanels.get(tick).first != null)
                                viewPanel.remove(boxPanels.get(tick).first);
                }
                else
                        boxPanels.put(tick, new Pair<FrameBoxPanel, FrameBoxPanel>());
                boxPanels.get(tick).first = boxPanel;
                viewPanel.add(boxPanel);
        }

        private void setMainBoxPanel(int tick, FrameBoxPanel boxPanel)
        {
                if(boxPanels.containsKey(tick))
                {
                        if(boxPanels.get(tick).second != null)
                                viewPanel.remove(boxPanels.get(tick).second);
                }
                else
                        boxPanels.put(tick, new Pair<FrameBoxPanel, FrameBoxPanel>());
                boxPanels.get(tick).second = boxPanel;
                if(boxPanel != null)
                        viewPanel.add(boxPanel);
        }

        private void removeAllBoxPanels(int tick)
        {
                Pair<FrameBoxPanel, FrameBoxPanel> boxPanelPair = boxPanels.get(tick);
                if(boxPanelPair.first != null)
                        viewPanel.remove(boxPanelPair.first);
                if(boxPanelPair.second != null)
                viewPanel.remove(boxPanelPair.second);
                boxPanels.remove(tick);
        }

        public synchronized void changed(TicksToXMapper source, IElement element, boolean added)
        {
                if(added)
                        addRelation(element, source);
                else
                        removeRelation(element);
        }

        private class FrameBoxPanel extends BasicNodePanel implements MouseListener
        {
                private MutableValueNode<Double> ratioNode;
                private boolean draw;
                private Color background;
                private Label label;

                public FrameBoxPanel(MutableValueNode<Double> xNode, MutableValueNode<Double> yNode,
                                     MutableValueNode<Double> widthNode, MutableValueNode<Double> heightNode,
                                     int tick, boolean pre)
                {
                        super(xNode, yNode, widthNode, heightNode);

                        final MutableValueNode<Double> effectiveXNode = new BasicValueNode<Double>(0.0);
                        MutableValueNode<Double> invertedNode = new BasicValueNode<Double>(0.0);
                        new DoubleInversionRelation(getXNode(), invertedNode);
                        new DoubleSumRelation(xNode, invertedNode, effectiveXNode);

                        final MutableValueNode<Double> dividedNode = new BasicValueNode<Double>(0.0);
                        preferredWidthNode.addObserver(new Observer()
                        {
                                public void onNotify(Object source)
                                {
                                        dividedNode.setValue(1.0 / preferredWidthNode.getValue());
                                }
                        });

                        ratioNode = new BasicValueNode<Double>(0.0);
                        new DoubleMultiplicationRelation(effectiveXNode, dividedNode, ratioNode);

                        drawBox(false);
                        addMouseListener(this);
                        setBackground(ViewConstants.MAIN_BACKGROUND_COLOR);
                        setSelected(false);

                        label = new Label(staffCollectionView.getStaffCollection()
                                          .getStaffCollectionId(), tick, pre)
                                          .setRatioNode(ratioNode);
                }

                @Override
                protected void paintComponent(Graphics g)
                {
                        super.paintComponent(g);
                        if(!draw && !isSelected())
                                return;
                        g.setColor(background);
                        g.fillRoundRect(0, (int) ViewConstants.FRAME_BOX_TOP_MARGIN,
                                widthNode.getValue().intValue(), (int) ViewConstants.FRAME_BOX_WIDTH,
                                ViewConstants.ROUNDRECT_ARC_WIDTH, ViewConstants.ROUNDRECT_ARC_HEIGHT);
                }

                private boolean isSelected()
                {
                        return background == ViewConstants.FRAME_BOX_SELECTED_COLOR;
                }

                public void setSelected(boolean selected)
                {
                        if(selected)
                        {
                                background = ViewConstants.FRAME_BOX_SELECTED_COLOR;
                                //viewPanel.getLeadSheet().addLabel(ratioNode);
                        }
                        else
                        {
                                background = ViewConstants.FRAME_BOX_COLOR;
                                //viewPanel.getLeadSheet().removeLabel(ratioNode);
                        }
                        repaint();
                }

                public void setLabel(Label label)
                {
                        this.label = label;
                        label.setRatioNode(ratioNode);
                }

                private void drawBox(boolean draw)
                {
                        this.draw = draw;
                        repaint();
                }

                public void mouseClicked(MouseEvent e)
                {
                        if(isSelected())
                                viewPanel.getLeadSheet().execute(
                                        new RemoveLabelCommand(viewPanel.getLeadSheet().getLabels(), label));
                        else
                                viewPanel.getLeadSheet().execute(
                                        new AddLabelCommand(viewPanel.getLeadSheet().getLabels(), label));
                }

                public void mouseEntered(MouseEvent e)
                {
                        drawBox(true);
                }

                public void mouseExited(MouseEvent e)
                {
                        drawBox(false);
                }

                public void mousePressed(MouseEvent e) {}
                public void mouseReleased(MouseEvent e) {}
        }
}